/*
 * KindlegenRunner.java
 *
 *  created: 30.8.2011
 *  charset: UTF-8
 *  license: MIT (X11) (See LICENSE file for full license)
 */
package cz.mp.k3bg.core;

import cz.mp.k3bg.Application;
import static cz.mp.k3bg.Application.EOL;
import cz.mp.k3bg.log.LoggerManager;
import cz.mp.util.Stopwatch;
import cz.mp.util.StringUtils;
import cz.mp.util.app.ApplicationUtils;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * Třída {@code KindlegenRunner} slouží pro vykonání programu {@code kindlegen}.
 * <p>
 * Podporuje {@code kindlegen} ve verzi 1.1, 1.2, 2.3.
 * Soubory knihy by měly být kódovány v {@code UTF-8}.
 * <p>
 * Příklad použití:
 * <tt><pre>
 * KindlegenRunner kindlegen = new KindlegenRunner();
 * kindlegen.setOpfFileNamePath("mybook"+FILE_SEPARATOR+"book.opf");
 * kindlegen.setOutputFileName("book.mobi");
 * String kindlegenCmd = kindlegen.findKindlegenCommand();
 * if (kindlegenCmd == null) {
 *     kindlegenCmd = "...";
 * }
 * if (kindlegenCmd != null &&
 *         kindlegen.isKindlegenCommand(kindlegenCmd) != null) {
 *     kindlegen.setBaseKindlegenCommand(kindlegenCmd);
 *     kindlegen.setKindlegenCommandOutputStream(System.out);
 *     kindlegen.run();
 *     System.err.println("kindlegen result: " +
 *             (kindlegen.isInErrorState() ? "ERROR" : "OK"));
 * }
 * </pre></tt>
 * 
 * @author Martin Pokorný
 * @version 0.5
 */
public class KindlegenRunner {
    
    private static final boolean DEBUG = false;
    private static final Logger logger =
            LoggerManager.getLogger(KindlegenRunner.class, DEBUG);
        
    /**
     * Verze programu kindlegen, do které (včetně) se pro 
     * použití unikódu (UTF-8) musí použít přepínač {@code -unicode}.
     * Vyšší verze automaticky pracují s unicode.
     */
    private static final String KINDLEGEN_NO_UNICODE_VERSION = "1.1";
            
    /** Příkaz {@code kindlegen}, s cestou k němu, pokud musí být. */
    private String kindlegenCommand = null;
    
    /** Příznak zda {@code kindlegen} skončil s chybou 
     * (řádek obsahuje text "{@code Error}"). */
    private boolean inErrorState = false;

    /** Doba běhu samotného programu {@code kindlegen}. */
    private long runtime = -1;
    
    private String kindlegenVersion = null;    
    
    private static final String KINDLEGEN_BASE_NAME = "kindlegen";
    
    /**
     * Seznam potencionálních spouštěčů (jejich umístění)
     * programu {@code kindlegen} v OS.
     * 
     */
    private static final String[] POTENTIAL_KINDLEGEN_CMDS = new String[] {
        // * obecně
        KINDLEGEN_BASE_NAME,
        // * MS-Windows:
        "C:\\Program Files\\KindleGen\\kindlegen.exe",
        "C:\\Program Files\\Amazon\\KindleGen\\kindlegen.exe",
        "C:\\Program Files\\"+Application.NAME+"\\kindlegen.exe",
        "C:\\Program Files\\"+Application.NAME_SHORT+"\\kindlegen.exe",
        // * Linux (Unix-like):
        "/opt/KindleGen/kindlegen",
        "/opt/kindlegen/kindlegen",
        "/opt/Kindlegen/kindlegen",
        "/opt/"+Application.NAME+"/kindlegen",
        "/opt/"+Application.NAME_SHORT+"/kindlegen",
        "/opt/Amazon/KindleGen/kindlegen",
    };    
    
    /** */
    private String opfFileNamePath;
    
    /** */
    private String outputFileName;
    
    /** Na tento proud se zapisuje výstup programu {@code kindlegen}. */
    private OutputStream kindlegenCommandOutputStream;
    
    /** Přítomnost přepínače {@literal -c2}. */
    private boolean c2compress = false;
    
    /** Proces OS, který vykonává {@code kindlegen} se zadanými parametry. */
    private Process process;

    // -----
    
    /**
     * 
     */
    public KindlegenRunner() {        
    }
    
    /**
     * 
     * @param opfFileNamePath
     * @param outputFileName 
     */
    public KindlegenRunner(String opfFileNamePath, String outputFileName) {
        setOpfFileNamePathImpl(opfFileNamePath);
        setOutputFileNameImpl(outputFileName);
    }

    /**
     * Vykoná {@code kindlegen} s parametry.
     * 
     * @return
     * @throws IOException 
     */
    public String[] run() throws IOException {
        logger.info("");
        if (StringUtils.isBlank(opfFileNamePath)) {
            logger.warning("opfFileNamePath is empty!");
            throw new IllegalStateException("opfFileNamePath is empty!");
        }
        if (StringUtils.isBlank(outputFileName)) {
            logger.warning("outputFileName is empty!");
            throw new IllegalStateException("outputFileName is empty!");
        }  
        if (StringUtils.isBlank(kindlegenCommand)) {
            logger.warning("kindlegenCommand is empty");
            throw new IllegalArgumentException("kindlegenCommand is empty");
        }

        
        List<String> messageLines = new ArrayList<String>();
        inErrorState = false;
        Stopwatch runtimeStopwatch = new Stopwatch();
        runtimeStopwatch.start();

        try {
            String[] realCommandArray = assembleKindlegenCommandArray();
            logger.info("command:  " + Arrays.toString(realCommandArray));
            String readableCommand =
                    convertCommandArrayToSingleCommand(realCommandArray);
            writeToCommandOut("> " + readableCommand);

            process = Runtime.getRuntime().exec(realCommandArray);

            InputStream istream = process.getInputStream();
            BufferedReader br = new BufferedReader(
                    new InputStreamReader(istream));

            String line;            
            while ((line = br.readLine()) != null) {
//                if (line.trim().isEmpty()) {
//                    continue;
//                }
                checkErrorState(line);
                messageLines.add(line);
                logger.fine(line);
//                logger.info(line);
                writeToCommandOut(line);
            }

            try {
                process.waitFor();
            } catch (InterruptedException e) {
                setInErrorState(true);
                writeToCommandOut(EOL+"Error: InterruptedException!");
            }

//            if (proc.exitValue() != 0) {      // kindlegen 1.2 vrátí vždy 0
//                writeToCommandOut(EOL+"Error: Exit code = "
//                        + proc.exitValue()"+EOL);
//            }
           
            br.close();
            
            logger.fine("finished!");
        } catch (IOException ex) {
            logger.warning(ex.toString());
            setInErrorState(true);
            runtime = runtimeStopwatch.stop();
            
            throw ex;
        }       
        runtime = runtimeStopwatch.stop();        
        logger.info("kindlegen runtime = " + 
                runtimeStopwatch.getTimeSec() + " s");
        
        return (String[]) messageLines.toArray(new String[0]);  
    }

    /**
     * Zastaví vykonávání {@code kindlegen}, pokud běží.
     *
     * @see #run() 
     */
    public void stop() {
        if (process == null) {
            logger.fine("process=null; skip");
        }
        else {
            logger.info("");
            process.destroy();
            setInErrorState(true);
        }
    }

    /**
     * 
     * @return  Doba běhu programu {@code kindlegen}, nebo -1, 
     *      pokud nebyl spuštěn.
     */
    public long getRuntime() {
        return runtime;
    }

    /**
     * 
     * @param line 
     */
    private void checkErrorState(String line) {
        if (line == null) {
            throw new IllegalStateException("line is null!");
        } 
        if (line.toLowerCase().startsWith("error")) {
            setInErrorState(true);
        }
    }
    
    /**
     * 
     * @return 
     */
    public boolean isInErrorState() {
        return inErrorState;
    }

    /**
     * 
     */
    private void setInErrorState(boolean inErrorState) {
        logger.fine("inErrorState = " + inErrorState);
        this.inErrorState = inErrorState;
    }
    

    /**
     * 
     * @param line
     * @throws IOException 
     */
    private void writeToCommandOut(String line) throws IOException {
        if (kindlegenCommandOutputStream != null) {
            kindlegenCommandOutputStream.write(line.trim().getBytes("UTF-8"));
            kindlegenCommandOutputStream.write(EOL.getBytes("UTF-8"));
        }
    }
    
    
    /**
     * Testuje, zda je zadané umístění příkazu, příkazem {@code kindlegen}.
     * 
     * @return  číslo verze programu {@code kindlegen} nebo {@code null},
     *      pokud zadaný příkaz není program {@code kindlegen}
     */
    public String isKindlegenCommand(String baseKindlegenCommand) {
        // nejprve je třeba odfiltrovat příkazy, které nemůžou být kindlegen, a
        //  mohou být i problémové (zaseknou tento program, 
        //  udělají něco co nemají atd.)
        String name = new File(baseKindlegenCommand).getName();
        if (StringUtils.isBlank(name) ||
                ! name.toLowerCase().contains(KINDLEGEN_BASE_NAME)) {
            return null;
        }

        BufferedReader br = null;
        String result = null;
        try {
            Process cProcess = Runtime.getRuntime().exec(baseKindlegenCommand);
            
            br = new BufferedReader(
                    new InputStreamReader(
                        cProcess.getInputStream()));
        
            String line;
            while ((line = br.readLine()) != null) {
                if (line.contains(KINDLEGEN_BASE_NAME)) {
                    result = parseKindlegenVersion(line);
                            
                    break;
                }
            }
            
        } catch (IOException ex) {
            // nic
        }
        finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException ex) {
                    // nic
                }
            }
        }
        
        if (! StringUtils.isBlank(result)) {
            logger.fine(baseKindlegenCommand + "  version=" + result);
        }
        else {
            logger.fine(baseKindlegenCommand + "  NO");
        }
        return result;        
    }
    
    /**
     * 
     * @param kgOutLine
     * @return 
     */
    private String parseKindlegenVersion(String kgOutLine) {
        String result = null;
        
        // např. V1.2 build 33307;  V1.1 build 99
        Pattern pattern = Pattern.compile("\\s*([\\d\\.]*)\\s*build\\s\\d+");
        Matcher matcher = pattern.matcher(kgOutLine);
        if (matcher.find()) {
            String v = matcher.group(1);
            if (! StringUtils.isBlank(v)) {
                result = v;
            }
        }
        
        return result;
    }
    
    /**
     * 
     * @return  umístění programu {@code kindlegen}, nebo {@code null}, 
     *      pokud nebyl {@code kindlegen} v obvyklých
     *      místech souborového systému nalezen
     */
    public String findKindlegenCommand() {
        logger.finer("");
        for (String command : POTENTIAL_KINDLEGEN_CMDS) {
            String v = isKindlegenCommand(command);
            if (v != null) {
                logger.config(command + "  version=" + v);
                kindlegenVersion = v;                
                return command;
            }
        }        
        return null;
    }
    

    /**
     * Sestaví příkaz pro spuštění programu {@code kindlegen} s
     * parametry podle této třídy.
     *
     * @return
     */
    private String[] assembleKindlegenCommandArray() {
        logger.fine("");

        ArrayList<String> cmds = new ArrayList<String>();
        cmds.add(kindlegenCommand);

        if (opfFileNamePath != null && ! opfFileNamePath.isEmpty()) {
            if (c2compress) {
                cmds.add("-c2");
            }
            if (isNoUnicodeVersion()) {
                cmds.add("-unicode");
            }
            cmds.add(opfFileNamePath);
            if (outputFileName != null && ! outputFileName.isEmpty()) {
                cmds.add("-o");
                cmds.add(outputFileName);
            }
        }
        else {
            return new String[0];
        }

        return cmds.toArray(new String[0]);
    }

    /**
     * 
     * @param array
     * @return  příkaz v podobě, kterou lze předat příkazovému řádku, nebo
     *      prázdný řetězec
     * @see #assembleKindlegenCommandArray() 
     */
    private String convertCommandArrayToSingleCommand(String[] array) {
        StringBuilder sb = new StringBuilder();
        for (String cmdPart : array) {
            if (cmdPart.contains(" ")) {
                sb.append("\"").append(cmdPart).append("\"");
            }
            else {
                sb.append(cmdPart);
            }
            sb.append(" ");
        }
        return sb.toString();
    }

    /**
     *
     * @return {@code true}, pokud zadaný program {@code kindlegen} je ve verzi,
     *      která ještě nepracuje v unicode ve výchozím stavu.
     * @see #KINDLEGEN_NO_UNICODE_VERSION
     */
    private boolean isNoUnicodeVersion() {
        int res = ApplicationUtils.compareVersions(
                kindlegenVersion, KINDLEGEN_NO_UNICODE_VERSION);
        return res <= 0;
    }
    
    /**
     * Získá umístění programu {@code kindlegen}, nebo {@code null}.
     * 
     * @return 
     */
    public String getBaseKindlegenCommand() {
        return kindlegenCommand;
    }

    
    /**
     * Nastaví umístění programu {@code kindlegen}.
     * 
     * @param kindlegenCommand 
     * @return  {@code true}, pokud je zadán příkaz programu {@code kindlegen},
     *      jinak {@code false}
     * @throws IllegalArgumentException  pokud je {@code kindlegenCommand} 
     *      prázdný 
     * @see #findKindlegenCommand() 
     */
    public boolean setBaseKindlegenCommand(String kindlegenCommand) {
        if (kindlegenCommand == null || kindlegenCommand.isEmpty()) {
            throw new IllegalArgumentException("baseKindlegenCommand = null");
        }
        String v = isKindlegenCommand(kindlegenCommand);
        if (! StringUtils.isBlank(v)) {
            kindlegenVersion = v;
            logger.config(kindlegenCommand + "  version=" + kindlegenVersion);
            this.kindlegenCommand = kindlegenCommand;
            return true;
        }
        else {
            logger.warning(kindlegenCommand + "  isn't kindlegen !");
            return false;
        }
    }

    /**
     * 
     * @return 
     */
    public String getKindlegenVersion() {
        return kindlegenVersion;
    }
    
    
    /**
     * 
     * @return 
     */
    public String getOutputFileName() {
        return outputFileName;
    }
    
    /**
     * 
     */
    public void setOutputFileName(String outputFileName) {
        setOutputFileNameImpl(outputFileName);
    }

    /**
     * 
     */
    private void setOutputFileNameImpl(String outputFileName) {
        if (outputFileName == null) {
            throw new IllegalArgumentException("outputFileName=null");
        }
        logger.config("outputFileName = " + outputFileName);
        this.outputFileName = outputFileName;
    }

    /**
     * 
     * @return 
     */
    public String getOpfFileNamePath() {
        return opfFileNamePath;
    }

    /**
     * 
     * @param opfFileNamePath 
     */
    public void setOpfFileNamePath(String opfFileNamePath) {
        setOpfFileNamePathImpl(opfFileNamePath);
    }

    /**
     * 
     * @param opfFileNamePath 
     */
    private void setOpfFileNamePathImpl(String opfFileNamePath) {
        if (opfFileNamePath == null) {
            throw new IllegalArgumentException("opfFileNamePath=null");
        }        
        logger.config("opfFileNamePath = " + opfFileNamePath);
        this.opfFileNamePath = opfFileNamePath;
    }    
    
    /**
     * 
     * @return 
     */
    public boolean isC2compress() {
        return c2compress;
    }

    /**
     * Nastavit vyšší kompresi. Odpovídá parametru {@literal -c2} 
     * programu {@code kindlegen}.
     * Způsobí pomalé sestavení.
     * 
     * @param c2compress 
     */
    public void setC2compress(boolean c2compress) {
        this.c2compress = c2compress;
    }

    /**
     * 
     * @return 
     */
    public OutputStream getKindlegenCommandOutputStream() {
        return kindlegenCommandOutputStream;
    }

    /**
     * Nastaví výstupní proud do něhož se bude průběžně zapisovat
     * std výstup programu {@code kindlegen} během vykonávání.
     * 
     * @param kindlegenCommandOutputStream  (může být např. System.out)
     */
    public void setKindlegenCommandOutputStream(
            OutputStream kindlegenCommandOutputStream) {
        if (kindlegenCommandOutputStream == null) {
            throw new IllegalArgumentException(
                    "kindlegenCommandOutputStream=null");
        }           
        this.kindlegenCommandOutputStream = kindlegenCommandOutputStream;
    }
    
}   // KindlegenRunner

// -----------------------------------------------------------------------------

//**************************************************
//* Amazon.com kindlegen(Linux) V2.4 build 69900   *
//* A command line e-book compiler                 *
//* Copyright Amazon.com 2012                      *
//**************************************************
//
//Usage : kindlegen filename.opf/.htm/.html/.epub [-c0 or -c1 or c2] [-verbose] [-western] [-o <file name>]
//Options:
//   -c0: no compression
//   -c1: standard DOC compression
//   -c2: Kindle huffdic compression
//   -o <file name>: Specifies the output file name. Output file will be created in the same directory as that of input file. <file name> should not contain directory path.
//   -verbose: provides more information during ebook conversion
//   -western: force build of Windows-1252 book
//   -releasenotes: display release notes
//   -gif: images are converted to GIF format (no JPEG in the book)
//   -locale <locale option> : To display messages in selected language
//      en: English
//      de: German
//      fr: French
//      it: Italian
//      es: Spanish
//      zh: Chinese
//      ja: Japanese

// -----------------------------------------------------------------------------

//**************************************************
//* Amazon.com kindlegen(Linux) V2.3 build 36043   *
//* A command line e-book compiler                 *
//* Copyright Amazon.com 2011                      *
//**************************************************
//
//Usage : kindlegen filename.opf/.htm/.html/.epub [-c0 or -c1 or c2] [-verbose][-rebuild] [-onlydeps or -nodeps] [-western] [-o <file name>] 
//Options: 
//   -c0: no compression 
//   -c1: standard DOC compression 
//   -c2: Kindle huffdic compression 
//   -o <file name>: Specifies the output file name. Output file will be created in the same directory as that of input file. <file name> should not contain directory path. 
//   -verbose: verbose output 
//   -rebuild: rebuilds all dependencies 
//   -onlydeps: build only needed dependencies 
//   -nodeps: do not check/build dependencies 
//   -western: force build of Windows-1252 book 
//   -releasenotes: display release notes 
//   -gif: images are converted to GIF format (no JPEG in the book) 
//   -locale <locale option> 
//      en: To display log messages in english.
//      de: Um Log-Meldungen in Deutsch.
//      fr: Pour afficher les messages du journal en français.
//      it: Per visualizzare i messaggi di log in italiano.
//      es: Para mostrar los mensajes de registro en español.

// -----------------------------------------------------------------------------

//**************************************************
//* Amazon.com kindlegen(Windows) V2.3 build 36043 *
//* A command line e-book compiler                 *
//* Copyright Amazon.com 2011                      *
//**************************************************
//
//Usage : kindlegen filename.opf/.htm/.html/.epub [-c0 or -c1 or c2] [-verbose][-rebuild] [-onlydeps or -nodeps] [-western] [-o <file name>] 
//Options: 
//   -c0: no compression 
//   -c1: standard DOC compression 
//   -c2: Kindle huffdic compression 
//   -o <file name>: Specifies the output file name. Output file will be created in the same directory as that of input file. <file name> should not contain directory path. 
//   -verbose: verbose output 
//   -rebuild: rebuilds all dependencies 
//   -onlydeps: build only needed dependencies 
//   -nodeps: do not check/build dependencies 
//   -western: force build of Windows-1252 book 
//   -releasenotes: display release notes 
//   -gif: images are converted to GIF format (no JPEG in the book) 
//   -locale <locale option> 
//      en: To display log messages in english.
//      de: Um Log-Meldungen in Deutsch.
//      fr: Pour afficher les messages du journal en français.
//      it: Per visualizzare i messaggi di log in italiano.
//      es: Para mostrar los mensajes de registro en español.

// -----------------------------------------------------------------------------

//**************************************************
//* Amazon.com kindlegen(Linux)   V1.2 build 33307 *
//* A command line e-book compiler                 *
//* Copyright Amazon.com 2011                      *
//**************************************************
//
//Usage : kindlegen filename.opf/.htm/.html/.epub [-c0 or -c1 or c2] [-verbose] [-rebuild] [-onlydeps or -nodeps] [-western] [-o <file name>]
//Options:
//   -c0: no compression
//   -c1: standard DOC compression
//   -c2: Kindle huffdic compression
//   -o <file name>: Specifies the output file name. Output file will be created in the same directory as that of input file. <file name> should not contain directory path.
//   -verbose: verbose output
//   -rebuild: rebuilds all dependencies
//   -onlydeps: build only needed dependencies
//   -nodeps: do not check/build dependencies
//   -western: force build of Windows-1252 book
//   -releasenotes: display release notes
//   -gif: images are converted to GIF format (no JPEG in the book)

// -----------------------------------------------------------------------------

//**************************************************
//* Amazon.com kindlegen(Windows) V1.2 build 33307 *
//* A command line e-book compiler                 *
//* Copyright Amazon.com 2011                      *
//**************************************************
//
//Usage : kindlegen filename.opf/.htm/.html/.epub [-c0 or -c1 or c2] [-verbose] [-rebuild] [-onlydeps or -nodeps] [-western] [-o <file name>]
//Options:
//   -c0: no compression
//   -c1: standard DOC compression
//   -c2: Kindle huffdic compression
//   -o <file name>: Specifies the output file name. Output file will be created in the same directory as that of input file. <file name> should not contain directory path.
//   -verbose: verbose output
//   -rebuild: rebuilds all dependencies
//   -onlydeps: build only needed dependencies
//   -nodeps: do not check/build dependencies
//   -western: force build of Windows-1252 book
//   -releasenotes: display release notes
//   -gif: images are converted to GIF format (no JPEG in the book)

// -----------------------------------------------------------------------------

//***********************************************
//* Amazon.com kindlegen(Windows) V1.1 build 99 *
//* A command line e-book compiler              *
//* Copyright Amazon.com 2010                   *
//***********************************************
//
//Usage : kindlegen filename.opf/.htm/.html/.epub [-lowpriority] [-nomin] [-c0 or -c1 or c2] [-verbose] [-nocopypaste] [-rebuild] [-onlydeps or -nodeps] [-unicode] [-o <file name>]
//Options:
//   -c0: no compression
//   -c1: standard DOC compression
//   -c2: Kindle huffdic compression
//   -o <file name>: create the output file named <file name>.Do not specify directory location in <file name>. Create output mobi file in same directory as .opf file. 
//   -verbose: verbose output
//   -nocopypaste: does not allow any copy paste of content in Reader
//   -nomin: do not minimize version
//   -rebuild: rebuilds all dependencies
//   -onlydeps: build only needed dependencies
//   -nodeps: do not check/build dependencies
//   -unicode: force build of Unicode book
//   -lowpriority: set the KINDLEGEN thread priority to low (background build)
//   -releasenotes: display release notes
//   -gif: images are converted to GIF format (no JPEG in the book)

// -----------------------------------------------------------------------------

// KindlegenRunner.java